package com.wangfugui.apprentice.common.util;

import com.alibaba.fastjson.JSON;
import com.google.common.collect.HashMultimap;
import com.wangfugui.apprentice.common.CustomMinioClient;
import com.wangfugui.apprentice.common.SplitFileDto;
import com.wangfugui.apprentice.dao.dto.Fileinfo;
import io.minio.BucketExistsArgs;
import io.minio.CopyObjectArgs;
import io.minio.CopySource;
import io.minio.GetObjectArgs;
import io.minio.GetPresignedObjectUrlArgs;
import io.minio.ListObjectsArgs;
import io.minio.ListPartsResponse;
import io.minio.MakeBucketArgs;
import io.minio.ObjectWriteResponse;
import io.minio.PutObjectArgs;
import io.minio.RemoveBucketArgs;
import io.minio.RemoveObjectArgs;
import io.minio.Result;
import io.minio.StatObjectArgs;
import io.minio.errors.ErrorResponseException;
import io.minio.errors.InsufficientDataException;
import io.minio.errors.InternalException;
import io.minio.errors.InvalidResponseException;
import io.minio.errors.ServerException;
import io.minio.errors.XmlParserException;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.Item;
import io.minio.messages.Part;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Component
public class MinioUtil {
    @Autowired
    private CustomMinioClient minioClient;

    /**
     * 创建一个桶
     */
    public void createBucket(String bucket) throws Exception {
        boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build());
        if (!found) {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build());
        }
    }

    /**
     * 上传一个文件
     */
    public void uploadFile(InputStream stream, String bucket, String objectName) throws Exception {
        minioClient.putObject(PutObjectArgs.builder().bucket(bucket).object(objectName)
                .stream(stream, -1, 10485760).build());
    }

    /**
     * 列出所有的桶
     */
    public List<String> listBuckets() throws Exception {
        List<Bucket> list = minioClient.listBuckets();
        List<String> names = new ArrayList<>();
        list.forEach(b -> {
            names.add(b.name());
        });
        return names;
    }

    /**
     * 列出一个桶中的所有文件和目录
     */
    public List<Fileinfo> listFiles(String bucket) throws Exception {
        Iterable<Result<Item>> results = minioClient.listObjects(
                ListObjectsArgs.builder().bucket(bucket).recursive(true).build());

        List<Fileinfo> infos = new ArrayList<>();
        results.forEach(r -> {
            Fileinfo info = new Fileinfo();
            try {
                Item item = r.get();
                info.setFilename(item.objectName());
                info.setDirectory(item.isDir());
                infos.add(info);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        return infos;
    }

    /**
     * 下载一个文件
     */
    public InputStream download(String bucket, String objectName) throws Exception {
        InputStream stream = minioClient.getObject(
                GetObjectArgs.builder().bucket(bucket).object(objectName).build());
        return stream;
    }

    /**
     * 删除一个桶
     */
    public void deleteBucket(String bucket) throws Exception {
        minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucket).build());
    }

    /**
     * 删除一个对象
     */
    public void deleteObject(String bucket, String objectName) throws Exception {
        minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucket).object(objectName).build());
    }


    /**
     * 复制文件
     *
     * @Param: [sourceBucket, sourceObject, targetBucket, targetObject]
     * @return: void
     * @Author: MrFugui
     * @Date: 2021/11/15
     */
    public void copyObject(String sourceBucket, String sourceObject, String targetBucket, String targetObject) throws Exception {
        this.createBucket(targetBucket);
        minioClient.copyObject(CopyObjectArgs.builder().bucket(targetBucket).object(targetObject)
                .source(CopySource.builder().bucket(sourceBucket).object(sourceObject).build()).build());
    }

    /**
     * 获取文件信息
     *
     * @Param: [bucket, objectName]
     * @return: java.lang.String
     * @Author: MrFugui
     * @Date: 2021/11/15
     */
    public String getObjectInfo(String bucket, String objectName) throws Exception {

        return minioClient.statObject(StatObjectArgs.builder().bucket(bucket).object(objectName).build()).toString();

    }

    /**
     * 生成一个给HTTP GET请求用的presigned URL。浏览器/移动端的客户端可以用这个URL进行下载，即使其所在的存储桶是私有的。
     *
     * @Param: [bucketName, objectName, expires]
     * @return: java.lang.String
     * @Author: MrFugui
     * @Date: 2021/11/15
     */
    public String getPresignedObjectUrl(String bucketName, String objectName, Integer expires) throws Exception {
        GetPresignedObjectUrlArgs build = GetPresignedObjectUrlArgs
                .builder().bucket(bucketName).object(objectName).expiry(expires).method(Method.GET).build();
        return minioClient.getPresignedObjectUrl(build);
    }

    /**
     * 获取minio中所有的文件
     *
     * @Param: []
     * @return: java.util.List<boot.spring.domain.Fileinfo>
     * @Author: MrFugui
     * @Date: 2021/11/15
     */
    public List<Fileinfo> listAllFile() throws Exception {
        List<String> list = this.listBuckets();
        List<Fileinfo> fileinfos = new ArrayList<>();
        for (String bucketName : list) {
            fileinfos.addAll(this.listFiles(bucketName));
        }


        return fileinfos;
    }


    public void splitFile() throws Exception {
        long CHUNK_SIZE = 30 * 1024 * 1024;
        // 将文件分片存储
        String filePath = "D:\\xiazai\\18a7bc018f35471f927ae205301c9442.mp4";
        File file = new File(filePath);
        long fileSize = file.length();
        int chunkCount = (int) Math.ceil((double) fileSize / CHUNK_SIZE);

        try (FileInputStream fis = new FileInputStream(file)) {
            byte[] buffer = new byte[(int) CHUNK_SIZE];
            for (int i = 0; i < chunkCount; i++) {
                String chunkFileName = filePath + ".part" + (i + 1);
                try (FileOutputStream fos = new FileOutputStream(chunkFileName)) {
                    int bytesRead = fis.read(buffer);
                    fos.write(buffer, 0, bytesRead);
                    System.out.println(chunkFileName);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

 public List<InputStream> splitFileToInputStreams(MultipartFile uploadfile) throws IOException {
    long CHUNK_SIZE = 5 * 1024 * 1024; // 每个分片的大小为5MB
    List<InputStream> inputStreams = new ArrayList<>(); // 用于存储分片的 InputStream

    try (BufferedInputStream bis = new BufferedInputStream(uploadfile.getInputStream())) {
        byte[] buffer = new byte[(int) CHUNK_SIZE];
        int bytesRead;
        while ((bytesRead = bis.read(buffer)) != -1) {
            byte[] chunkData = new byte[bytesRead];
            System.arraycopy(buffer, 0, chunkData, 0, bytesRead);
            InputStream chunkInputStream = new ByteArrayInputStream(chunkData);
            inputStreams.add(chunkInputStream);
        }
    }

    return inputStreams; // 返回分片的 InputStream 列表
}

    /**
     * 第二步，申请一个大文件上传
     * 该函数用于申请一个大文件的分块上传，生成每个分块的预签名URL，并返回包含上传ID和分块URL列表的DTO对象。
     *
     * @param fileName 文件名，用于生成上传ID和预签名URL
     * @param chunkCount 文件分块的总数，用于生成对应数量的预签名URL
     * @param bucketName MinIO存储桶的名称，用于指定文件上传的目标存储桶
     * @return SplitFileDto 包含上传ID和分块预签名URL列表的DTO对象
     * @throws ServerException 服务器异常
     * @throws InsufficientDataException 数据不足异常
     * @throws ErrorResponseException 错误响应异常
     * @throws IOException IO异常
     * @throws NoSuchAlgorithmException 无此算法异常
     * @throws InvalidKeyException 无效密钥异常
     * @throws XmlParserException XML解析异常
     * @throws InvalidResponseException 无效响应异常
     * @throws InternalException 内部异常
     */
    public SplitFileDto applyUploadPsiResult2Minio(String fileName, Integer chunkCount, String bucketName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, XmlParserException, InvalidResponseException, InternalException {
        // 获取文件上传的唯一ID
        String uploadId = getUploadId(fileName, bucketName);
        SplitFileDto splitFileDto = new SplitFileDto();
        Map<String, String> reqParams = new HashMap<>();
        splitFileDto.setUploadId(uploadId);
        reqParams.put("uploadId", uploadId);

        // 生成每个分块的预签名URL
        List<String> uploadUrlList = new ArrayList<>();
        for (int i = 1; i <= chunkCount; i++) {
            reqParams.put("partNumber", String.valueOf(i));
            String uploadUrl = getPresignedObjectUrl(fileName, reqParams, bucketName);
            uploadUrlList.add(uploadUrl);
        }

        // 设置分块预签名URL列表并返回DTO对象
        splitFileDto.setChunkUploadUrls(uploadUrlList);
        return splitFileDto;
    }


    /**
     * 准备分片上传时，在此先获取上传任务id
     */
    private String getUploadId(String objectName, String bucketName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, XmlParserException, InvalidResponseException, InternalException {
        String contentType = "application/octet-stream";
        HashMultimap<String, String> headers = HashMultimap.create();
        headers.put("Content-Type", contentType);
        return minioClient.initMultiPartUpload(bucketName, null, objectName, headers, null);
    }


    /**
     * 获取分片上传的预签名URL。
     *
     * 该方法通过MinIO客户端生成一个用于分片上传的预签名URL。预签名URL允许客户端在指定的时间内直接上传文件到指定的存储桶，而无需通过服务器进行身份验证。
     *
     * @param fileName 要上传的文件名，该文件名将作为对象存储在MinIO中。
     * @param reqParams 额外的查询参数，这些参数将包含在生成的预签名URL中。
     * @param bucketName 目标存储桶的名称，文件将被上传到该存储桶中。
     * @return 返回一个预签名的URL，客户端可以使用该URL直接上传文件。
     * @throws ServerException 如果与MinIO服务器的通信失败。
     * @throws InsufficientDataException 如果从服务器接收的数据不完整。
     * @throws ErrorResponseException 如果服务器返回错误响应。
     * @throws IOException 如果发生I/O错误。
     * @throws NoSuchAlgorithmException 如果请求的加密算法不可用。
     * @throws InvalidKeyException 如果提供的密钥无效。
     * @throws InvalidResponseException 如果服务器返回的响应无效。
     * @throws XmlParserException 如果解析XML响应时发生错误。
     * @throws InternalException 如果发生内部错误。
     */
    private String getPresignedObjectUrl(String fileName, Map<String, String> reqParams, String bucketName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        // 使用MinIO客户端生成预签名URL，设置HTTP方法为PUT，并指定文件、存储桶、过期时间及额外查询参数
        return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                    .method(Method.PUT)
                    .bucket(bucketName)
                    .object(fileName)
                    .expiry(1, TimeUnit.DAYS)
                    .extraQueryParams(reqParams)
                    .build());
    }


    /**
     * 将分片文件逐个上传到指定的URL。每个分片文件通过HTTP PUT请求直接上传到MinIO存储，避免通过服务端转发，减少网络IO开销。
     * 在文件合并之前，分片文件可以重复覆盖上传。
     *
     * @param uploadUrlList 包含每个分片文件上传URL的列表，URL已签名。
     * @param chunkFiles    包含每个分片文件输入流的列表，每个输入流对应一个分片文件。
     * @throws IOException 如果在上传过程中发生IO错误，则抛出此异常。
     */
    public void upload(List<String> uploadUrlList, List<InputStream> chunkFiles) throws IOException {

        // 创建默认的HTTP客户端，用于执行上传请求
        CloseableHttpClient httpClient = HttpClients.createDefault();

        // 遍历每个分片文件的上传URL和对应的输入流
        for (int i = 0; i < uploadUrlList.size(); i++) {
            // 创建HTTP PUT请求，将分片文件上传到指定的URL
            String chunkUploadUrl = uploadUrlList.get(i);
            HttpPut httpPut = new HttpPut(chunkUploadUrl);
            httpPut.setHeader("Content-Type", "video/mp4");

            // 生成随机的文件名，并设置Content-Disposition头
            UUID uuid = UUID.randomUUID();
            //todo 可以根据自己的文件加后缀
            String name = uuid + ".mp4";
            httpPut.addHeader("Content-Disposition", "filename=" + urlEncode(name, "UTF-8"));

            // 将输入流转换为字节数组，并设置为请求体
            byte[] chunkData = toByteArray(chunkFiles.get(i));
            ByteArrayEntity byteArrayEntity = new ByteArrayEntity(chunkData);
            httpPut.setEntity(byteArrayEntity);

            // 执行上传请求，并获取响应
            CloseableHttpResponse chunkUploadResp = httpClient.execute(httpPut);

            // 打印上传响应信息
            System.out.println("[分片" + (i + 1) + "]上传响应：" + JSON.toJSONString(chunkUploadResp));

            // 释放连接资源
            httpPut.releaseConnection();
        }
    }

    /**
     * 将输入流（InputStream）转换为字节数组（byte[]）。
     *
     * 该函数通过读取输入流中的数据，并将其写入到字节数组输出流（ByteArrayOutputStream）中，
     * 最终将输出流的内容转换为字节数组返回。
     *
     * @param inputStream 要转换的输入流，不能为null。
     * @return 包含输入流数据的字节数组。
     * @throws IOException 如果读取输入流时发生I/O错误。
     */
    public static byte[] toByteArray(InputStream inputStream) throws IOException {
        // 创建一个字节数组输出流，用于缓存从输入流读取的数据
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        int nRead;
        byte[] data = new byte[1024];

        // 从输入流中读取数据，直到读取到流的末尾（返回-1）
        while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
            // 将读取到的数据写入到字节数组输出流中
            buffer.write(data, 0, nRead);
        }

        // 确保所有数据都已写入到输出流中
        buffer.flush();

        // 将字节数组输出流的内容转换为字节数组并返回
        return buffer.toByteArray();
    }

    /**
     * 对给定的字符串进行URL编码，并使用指定的字符编码。
     * 编码过程中，会将字符串中的特殊字符替换为对应的百分号编码形式。
     * 如果输入的字符串为null，则返回空字符串。
     *
     * @param value 需要编码的字符串，可以为null
     * @param encoding 使用的字符编码，例如"UTF-8"
     * @return 编码后的字符串，如果输入为null则返回空字符串
     * @throws IllegalArgumentException 如果指定的字符编码不被支持
     */
    public static String urlEncode(String value, String encoding) {
        if (value == null) {
            return "";
        }

        try {
            // 使用URLEncoder对字符串进行编码
            String encoded = URLEncoder.encode(value, encoding);

            // 替换编码后的字符串中的特定字符为对应的百分号编码形式
            return encoded.replace("+", "%20").replace("*", "%2A").replace("~", "%7E").replace("/", "%2F");
        } catch (UnsupportedEncodingException e) {
            // 如果指定的字符编码不被支持，抛出IllegalArgumentException异常
            throw new IllegalArgumentException();
        }
    }
    /**
     * 分片上传完后合并
     *
     * 该函数用于在分片上传完成后，将所有分片合并为一个完整的对象。
     *
     * @param objectName 对象名称，即上传的文件名
     * @param uploadId 上传任务的唯一标识符
     * @param bucketName 存储桶名称，即文件存储的容器
     * @return ObjectWriteResponse 对象写入响应，包含合并后的对象信息
     * @throws ServerException 服务器异常
     * @throws InsufficientDataException 数据不足异常
     * @throws ErrorResponseException 错误响应异常
     * @throws IOException 输入输出异常
     * @throws NoSuchAlgorithmException 无此算法异常
     * @throws InvalidKeyException 无效密钥异常
     * @throws XmlParserException XML解析异常
     * @throws InvalidResponseException 无效响应异常
     * @throws InternalException 内部异常
     */
    public ObjectWriteResponse mergeMultipartUpload(String objectName, String uploadId, String bucketName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, XmlParserException, InvalidResponseException, InternalException {
        System.out.println("ready to merge <" + objectName + " - " + uploadId + " - " + bucketName + ">");

        // 查询上传后的分片数据
        ListPartsResponse partResult = minioClient.listMultipart(bucketName, null, objectName, 1000, 0, uploadId, null, null);
        int chunkCount = partResult.result().partList().size();
        Part[] parts = new Part[chunkCount];
        int partNumber = 1;

        // 将分片数据转换为合并所需的格式
        for (Part part : partResult.result().partList()) {
            parts[partNumber - 1] = new Part(partNumber, part.etag());
            partNumber++;
        }

        // 合并分片并返回合并后的对象信息
        ObjectWriteResponse objectWriteResponse = minioClient.mergeMultipartUpload(bucketName, null, objectName, uploadId, parts, null, null);
        return objectWriteResponse;
    }

}
